package com.bdqn.qnnews.wemedia.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.bdqn.qnnews.aliyungreen.utils.AliYunGreenUtil;
import com.bdqn.qnnews.articel.api.dto.ApArticleDto;
import com.bdqn.qnnews.articel.api.feign.ArticleFeignClient;
import com.bdqn.qnnews.common.base.R;
import com.bdqn.qnnews.common.constants.NewsStatus;
import com.bdqn.qnnews.common.constants.RedisConstants;
import com.bdqn.qnnews.common.utils.SensitiveWordUtil;
import com.bdqn.qnnews.minio.utils.MinioUtil;
import com.bdqn.qnnews.wemedia.convert.WmNewsConvert;
import com.bdqn.qnnews.wemedia.entity.WmChannel;
import com.bdqn.qnnews.wemedia.entity.WmNews;
import com.bdqn.qnnews.wemedia.entity.WmSensitive;
import com.bdqn.qnnews.wemedia.entity.WmUser;
import com.bdqn.qnnews.wemedia.mapper.WmChannelMapper;
import com.bdqn.qnnews.wemedia.mapper.WmNewsMapper;
import com.bdqn.qnnews.wemedia.mapper.WmSensitiveMapper;
import com.bdqn.qnnews.wemedia.mapper.WmUserMapper;
import com.bdqn.qnnews.wemedia.service.WmNewsAutoScanService;
import com.bdqn.qnnews.wemedia.service.WmNewsTaskService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.sourceforge.tess4j.ITesseract;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 文章自动审查服务实现类
 * @author xlzhang
 */
@Slf4j
@RequiredArgsConstructor
@Service
public class WmNewsAutoAutoScanServiceImpl implements WmNewsAutoScanService {

    private final WmNewsMapper wmNewsMapper;
    private final AliYunGreenUtil aliYunGreenUtil;
    private final WmChannelMapper wmChannelMapper;
    private final WmUserMapper wmUserMapper;
    private final ArticleFeignClient articleFeignClient;
    private final WmSensitiveMapper sensitiveMapper;
    private final StringRedisTemplate redisTemplate;
    private final MinioUtil minioUtil;
    private final ITesseract tesseract;
    private final WmNewsTaskService taskService;


    // 开启异步执行
    @Async
    @Override
    public void autoScanWmNews(Long id) {
        //根据id查询自媒体文章
        final WmNews wmNews = wmNewsMapper.selectById(id);
        if (wmNews == null){
            log.error("文章id异常");
            return;
        }

        //抽取文章的文本内容
        final String scanText = getTextFromWmNews(wmNews);
        // 先执行自定义的敏感词审核功能，如果敏感词都有问题，就不需要再给阿里云
        final boolean sensitiveScanRes = handleSensitiveScan(scanText, wmNews);
        if (!sensitiveScanRes){
            return;
        }

        //提交文本给阿里云内容检测，根据检测结果修改文章的状态
        //if (StringUtils.isNotEmpty(scanText)){
        //    final GreenScanResult result = aliYunGreenUtil.greenText(scanText);
        //    if (result.getData() == null) {return;}
        //    log.warn("文章内容审核反馈了异常信息：{}",result.getData());
        //    if (StringUtils.isNotEmpty(result.getData().getLabels())){
        //        // 将审核的异常原因保存到数据库，并修改状态
        //        // 不为空说明文本审查异常
        //        //修改文章状态为2
        //        wmNews.setStatus(NewsStatus.FAIL.getCode());
        //        wmNews.setReason(result.getData().getReason());
        //        wmNewsMapper.updateById(wmNews);
        //        return;
        //    }
        //
        //}else {
        //    log.error("文章内容提取失败，审核失败");
        //    return;
        //}
        //try {
        //    log.debug("测试---进入睡眠30秒");
        //    Thread.sleep(30000);
        //} catch (InterruptedException e) {
        //    e.printStackTrace();
        //}
        //log.debug("测试---睡眠30秒结束");
        //抽取文章的图片内容
        //final Set<String> images = getImageUrlFromWmNews(wmNews);
        // 判断如果文章带有图片，将状态设置为人工审查
        //if (images.size() > 0){
        //    //修改文章状态为2
        //    wmNews.setStatus(NewsStatus.ADMIN_AUTH.getCode());
        //    wmNews.setReason("包含图片，图片未进行自动审查，需要人工进一步审查");
        //    wmNewsMapper.updateById(wmNews);
        //    return;
        //}
        //提交图片给阿里云内容检测，根据检测结果修改文章的状态

        //判断发布时间是否大于当前时间，大于代表定时发布，修改文章状态 8
        if(wmNews.getPublishTime()!=null && wmNews.getPublishTime().isAfter(LocalDateTime.now())){
            //修改状态为8，暂时不发布
            wmNews.setStatus(NewsStatus.SUCCESS.getCode());
            wmNews.setReason("已审核通过，进入定时发布");
            wmNewsMapper.updateById(wmNews);
            log.info("已审核通过，进入定时发布");

            final Long taskId = taskService.addWmNewsTask(wmNews);
            log.info("远程执行定时任务添加功能成功: taskId = {}",taskId);
            return;
        }

        publishApAticle(wmNews);
    }

    /**
     * 发布文章功能
     * @param wmNews 文章数据
     */
    @Override
    public void publishApAticle(WmNews wmNews) {
        log.debug("已审核通过,发起远程调用传递数据给app文章微服务");
        // 把自媒体文章导入到App文章库，修改文章状态为 9，代表发表完成
        final R<Long> res = saveApArticle(wmNews);
        if(!res.getCode().equals(200)){
            throw new RuntimeException("文章自动审核时保存app端相关文章数据失败");
        }
        log.debug("app文章微服务执行完成，无异常");
        //回填article_id
        wmNews.setArticleId(res.getData());
        wmNews.setStatus(NewsStatus.PUBLISHED.getCode());
        wmNews.setReason("审核成功");
        wmNewsMapper.updateById(wmNews);
        log.debug("文章审核并发布成功：文章id={}",res.getData());
    }

    /**
     * 自定义敏感词检测
     * @param scanText
     * @param wmNews
     * @return
     */
    private boolean handleSensitiveScan(String scanText, WmNews wmNews) {

        List<String> sensitiveList = null;
        // 1. 先查询redis中是否缓存有敏感词列表数据
        // 注意，在管理端执行敏感词增删改时，要记得情况缓存，否则这里一致使用的都会是旧的数据
        final String sensitiveWords = redisTemplate.opsForValue().get(RedisConstants.SENSITIVE_WORD);
        // 2. 如果没有查到，就需要从mysql数据库查询
        if (StringUtils.isEmpty(sensitiveWords)) {
            // 先查询数据库，获取所有的敏感词
            final List<WmSensitive> wmSensitives = sensitiveMapper.selectList(null);
            // 将对象集合转换为指定成员的字符串集合
            sensitiveList = wmSensitives.stream()
                    .map(WmSensitive::getSensitives)
                    .collect(Collectors.toList());
            // 3. 将查询并处理后的敏感词列表数据缓存到redis
            redisTemplate.opsForValue().set(RedisConstants.SENSITIVE_WORD,JSONUtil.toJsonStr(sensitiveList));
        }else {
            // 将redis查询到的字符串转换为list
            sensitiveList = JSONUtil.toList(sensitiveWords, String.class);
        }
        // 调用DFA工具类初始化Map数据结构
        SensitiveWordUtil.initMap(sensitiveList);
        if (StringUtils.isNotEmpty(scanText)) {
            final Map<String, Integer> result = SensitiveWordUtil.matchWords(scanText);
            if(result.size() > 0){
                //获取违规词
                String keys = String.join(",",result.keySet());
                //修改文章状态
                wmNews.setStatus(NewsStatus.FAIL.getCode());
                wmNews.setReason("文章包含违规词："+ keys);
                wmNewsMapper.updateById(wmNews);
                log.warn("当前文章包含自定义敏感词{}",keys);
                return false;
            }
        }
        return true;
    }

    /**
     * 保存app端相关的文章数据
     * @param wmNews 自媒体文章对象
     */
    private R<Long> saveApArticle(WmNews wmNews) {
        // 类型转换
        final ApArticleDto apArticleDto = WmNewsConvert.INSTANCE.entityToArticleDto(wmNews);

        // 这里转换后apArticleDto里的默认id就是自媒体文章的id，不能直接传递给app端的文章
        // app端保存文章数据时会根据传过去的dto中的id判断当前是添加还是修改，所以这里dto的id应该是app的文章id
        // 我们从news表中可以看到有一个article_id，这个字段会在文章发布成功后回填app的文章id
        // 因此当前必须讲dto中的id设置为wmNews中的articleId，如果是修改就会有articleId，否则就是null
        apArticleDto.setId(wmNews.getArticleId());

        //频道
        WmChannel wmChannel = wmChannelMapper.selectById(wmNews.getChannelId());
        if(wmChannel != null){
            apArticleDto.setChannelName(wmChannel.getName());
        }

        //作者
        WmUser wmUser = wmUserMapper.selectById(wmNews.getUserId());
        if(wmUser != null){
            apArticleDto.setAuthorName(wmUser.getName());
        }

        // 保存或修改文章
        return articleFeignClient.saveAndUpdate(apArticleDto);
    }

    /**
     * 提取文章中的图片，包含封面和内容图片
     * @param wmNews 文章对象
     * @return 图片地址集合
     */
    private Set<String> getImageUrlFromWmNews(WmNews wmNews) {
        List<byte[]> imageList = new ArrayList<>();

        //注意：使用Set自动去重
        Set<String> urlList = new HashSet<>();

        //内容的图片路径
        if(StringUtils.isNotEmpty(wmNews.getContent())){
            List<Map> list = JSONUtil.toList(wmNews.getContent(), Map.class);
            for(Map map : list){
                if("image".equals(map.get("type"))){
                    urlList.add((String)map.get("value"));
                }
            }
        }
        //封面的图片路径
        if(StringUtils.isNotEmpty(wmNews.getImages())){
            String[] array = wmNews.getImages().split(",");
            urlList.addAll(Arrays.asList(array));
        }

        return urlList;
    }
    /**
     * 提取文章中的文字
     * @param wmNews 文章对象
     * @return 文章中的文本内容
     */
    private String getTextFromWmNews(WmNews wmNews) {
        // 保存所有文本内容的可变字符串
        final StringBuilder builder = new StringBuilder();
        //1. 标题
        if(StringUtils.isNotEmpty(wmNews.getTitle())){
            builder.append(wmNews.getTitle());
        }

        //2。 内容
        if(StringUtils.isNotEmpty(wmNews.getContent())){
            List<Map> list = JSONUtil.toList(wmNews.getContent(), Map.class);
            for(Map map : list){
                if("text".equals(map.get("type"))){
                    builder.append((String)map.get("value"));
                }
            }
        }

        //3. 标签
        if(StringUtils.isNotEmpty(wmNews.getLabels())){
            builder.append(wmNews.getLabels());
        }

        // 4. 提取图片文字内容
        // 4.1 获取文章内的所有图片
        final Set<String> imageUrls = getImageUrlFromWmNews(wmNews);
        try {
            if (CollectionUtil.isNotEmpty(imageUrls)){
                for (String imageUrl : imageUrls) {
                    // 通过minio下载图片
                    final byte[] bytes = minioUtil.downLoadFile(imageUrl);
                    // 将下载的图片数据转换为img对象
                    InputStream inputStream = new ByteArrayInputStream(bytes);
                    final BufferedImage image = ImageIO.read(inputStream);
                    if (image == null) {
                        continue;
                    }
                    // 通过Tess4J识别图片，获取文本内容
                    String str = tesseract.doOCR(image);
                    // 图片提取文字可能会提取很多的非文字字符，但是非文字字符不需要审核
                    // 为了处理非文字字符过多造成的文文本过长审核失败的问题，可以将非文字内容进行过滤
                    str = str.replaceAll("[^\u4E00-\u9FA5A-Za-z0-9]+","");
                    // 将获取的图片文本内容添加到字符串缓冲区
                    builder.append(str);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }


        return builder.toString();
    }
}